上一篇文章:MongoDB指南---11、使用复合索引、$操作符如何使用索引、索引对象和数组、索引基数
下一篇文章:MongoDB指南---13、索引类型
使用explain()和hint()
从上面的内容可以看出,explain()能够提供大量与查询相关的信息。对于速度比较慢的查询来说,这是最重要的诊断工具之一。通过查看一个查询的explain()输出信息,可以知道查询使用了哪个索引,以及是如何使用的。对于任意查询,都可以在最后添加一个explain()调用(与调用sort()或者limit()一样,不过explain()必须放在最后)。
最常见的explain()输出有两种类型:使用索引的查询和没有使用索引的查询。对于特殊类型的索引,生成的查询计划可能会有些许不同,但是大部分字段都是相似的。另外,分片返回的是多个explain()的聚合(第13章会介绍),因为查询会在多个服务器上执行。
不使用索引的查询的exlpain()是最基本的explain()类型。如果一个查询不使用索引,是因为它使用了"BasicCursor"(基本游标)。反过来说,大部分使用索引的查询使用的是BtreeCursor(某些特殊类型的索引,比如地理空间索引,使用的是它们自己类型的游标)。
对于使用了复合索引的查询,最简单情况下的explain()输出如下所示:
> db.users.find({"age" : 42}).explain()
{
"cursor" : "BtreeCursor age_1_username_1",
"isMultiKey" : false,
"n" : 8332,
"nscannedObjects" : 8332,
"nscanned" : 8332,
"nscannedObjectsAllPlans" : 8332,
"nscannedAllPlans" : 8332,
"scanAndOrder" : false,
"indexOnly" : false,
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 91,
"indexBounds" : {
"age" : [
[
42,
42
]
],
"username" : [
[
{
"$minElement" : 1
},
{
"$maxElement" : 1
}
]
]
},
"server" : "ubuntu:27017"
}
从输出信息中可以看到它使用的索引是age_1_username_1。"millis"表明了这个查询的执行速度,时间是从服务器收到请求开始一直到发出响应为止。然而,这个数值不一定真的是你希望看到的值。如果MongoDB尝试了多个查询计划,那么"millis"显示的是这些查询计划花费的总时间,而不是最优查询计划所花的时间。
接下来是实际返回的文档数量:"n"。它无法反映出MongoDB在执行这个查询的过程中所做的工作:搜索了多少索引条目和文档。索引条目是使用"nscanned"描述的。"nscannedObjects"字段的值就是所扫描的文档数量。最后,如果要对结果集进行排序,而MongoDB无法对排序使用索引,那么"scanAndOrder"的值就会是true。也就是说,MongoDB不得不在内存中对结果进行排序,这是非常慢的,而且结果集的数量要比较小。
现在你已经知道这些基础知识了,接下来依次详细介绍这些字段。
- "cursor" : "BtreeCursor age_1_username_1"
BtreeCursor表示本次查询使用了索引,具体来说,是使用了"age"和"username"上的索引{"age" : 1, "username" : 1}。如果查询要对结果进行逆序遍历,或者是使用了多键索引,就可以在这个字段中看到"reverse"和"multi"这样的值。
- "isMultiKey" : false
用于说明本次查询是否使用了多键索引(详见5.1.4节)。
- "n" : 8332
本次查询返回的文档数量。
- "nscannedObjects" : 8332
这是MongoDB按照索引指针去磁盘上查找实际文档的次数。如果查询包含的查询条件不是索引的一部分,或者说要求返回不在索引内的字段,MongoDB就必须依次查找每个索引条目指向的文档。
- "nscanned" : 8332
如果有使用索引,那么这个数字就是查找过的索引条目数量。如果本次查询是一次全表扫描,那么这个数字就表示检查过的文档数量。
- "scanAndOrder" : false
MongoDB是否在内存中对结果集进行了排序。
- "indexOnly" : false
MongoDB是否只使用索引就能完成此次查询(详见“覆盖索引”部分)。
在本例中,MongoDB只使用索引就找到了全部的匹配文档,从"nscanned"和"n"相等就可以看出来。然而,本次查询要求返回匹配文档中的所有字段,而索引只包含"age"和"username"两个字段。如果将本次查询修改为({"_id" : 0, "age" : 1, "username" : 1}),那么本次查询就可以被索引覆盖了,"indexOnly"的值就会是true。
- "nYields" : 0
为了让写入请求能够顺利执行,本次查询暂停的次数。如果有写入请求需要处理,查询会周期性地释放它们的锁,以便写入能够顺利执行。然而,在本次查询中,没有写入请求,因为查询没有暂停过。
- "millis" : 91
数据库执行本次查询所耗费的毫秒数。这个数字越小,说明查询效率越高。
- "indexBounds" : {...}
这个字段描述了索引的使用情况,给出了索引的遍历范围。由于查询中的第一个语句是精确匹配,因此索引只需要查找42这个值就可以了。本次查询没有指定第二个索引键,因此这个索引键上没有限制,数据库会在"age"为42的条目中将用户名介于负无穷("$minElement" : 1)和正无穷("$maxElement" : 1)的条目都找出来。
再来看一个稍微复杂点的例子:假如有一个{"user name" : 1, "age" : 1}上的索引和一个 {"age" : 1, "username" : 1}上的索引。同时查询"username"和"age"时,会发生什么情况?呃,这取决于具体的查询:
> db.c.find({age : {$gt : 10}, username : "sally"}).explain()
{
"cursor" : "BtreeCursor username_1_age_1",
"indexBounds" : [
[
{
"username" : "sally",
"age" : 10
},
{
"username" : "sally",
"age" : 1.7976931348623157e+308
}
]
],
"nscanned" : 13,
"nscannedObjects" : 13,
"n" : 13,
"millis" : 5
}
由于在要在"username"上执行精确匹配,在"age"上进行范围查询,因此,数据库选择使用{"username" : 1, "age" : 1}索引,这与查询语句的顺序相反。另一方面来说,如果需要对"age"精确匹配而对"username"进行范围查询,MongoDB就会使用另一个索引:
> db.c.find({"age" : 14, "username" : /.*/}).explain()
{
"cursor" : "BtreeCursor age_1_username_1 multi",
"indexBounds" : [
[
{
"age" : 14,
"username" : ""
},
{
"age" : 14,
"username" : {
}
}
],
[
{
"age" : 14,
"username" : /.*/
},
{
"age" : 14,
"username" : /.*/
}
]
],
"nscanned" : 2,
"nscannedObjects" : 2,
"n" : 2,
"millis" : 2
}
如果发现MongoDB使用的索引与自己希望它使用的索引不一致,可以使用hit()强制MongoDB使用特定的索引。例如,如果希望MongoDB在上个例子的查询中使用{"username" : 1, "age" : 1}索引,可以这么做:
> db.c.find({"age" : 14, "username" : /.*/}).hint({"username" : 1, "age" : 1})
如果查询没有使用你希望它使用的索引,于是你使用hint强制MongoDB使用某个索引,那么应该在应用程序部署之前在所指定的索引上执行explain()。如果强制MongoDB在某个查询上使用索引,而这个查询不知道如何使用这个索引,这样会导致查询效率降低,还不如不使用索引来得快。
查询优化器
MongoDB的查询优化器与其他数据库稍有不同。基本来说,如果一个索引能够精确匹配一个查询(要查询"x",刚好在"x"上有一个索引),那么查询优化器就会使用这个索引。不然的话,可能会有几个索引都适合你的查询。MongoDB会从这些可能的索引子集中为每次查询计划选择一个,这些查询计划是并行执行的。最早返回100个结果的就是胜者,其他的查询计划就会被中止。
这个查询计划会被缓存,这个查询接下来都会使用它,直到集合数据发生了比较大的变动。如果在最初的计划评估之后集合发生了比较大的数据变动,查询优化器就会重新挑选可行的查询计划。建立索引时,或者是每执行1000次查询之后,查询优化器都会重新评估查询计划。
explain()输出信息里的"allPlans"字段显示了本次查询尝试过的每个查询计划。
何时不应该使用索引
提取较小的子数据集时,索引非常高效。也有一些查询不使用索引会更快。结果集在原集合中所占的比例越大,索引的速度就越慢,因为使用索引需要进行两次查找:一次是查找索引条目,一次是根据索引指针去查找相应的文档。而全表扫描只需要进行一次查找:查找文档。在最坏的情况下(返回集合内的所有文档),使用索引进行的查找次数会是全表扫描的两倍,效率会明显比全表扫描低很多。
可惜,并没有一个严格的规则可以告诉我们,如何根据数据大小、索引大小、文档大小以及结果集的平均大小来判断什么时候索引很有用,什么时候索引会降低查询速度(如表5-1所示)。一般来说,如果查询需要返回集合内30%的文档(或者更多),那就应该对索引和全表扫描的速度进行比较。然而,这个数字可能会在2%~60%之间变动。
表5-1 影响索引效率的属性
索引通常适用的情况 | 全表扫描通常适用的情况 |
---|---|
集合较大 | 集合较小 |
文档较大 | 文档较小 |
选择性查询 | 非选择性查询 |
假如我们有一个收集统计信息的分析系统。应用程序要根据给定账户去系统中查询所有文档,根据从初始一直到一小时之前的数据生成图表:
> db.entries.find({"created_at" : {"$lt" : hourAgo}})
我们在"created_at"上创建索引以提高查询速度。
最初运行时,结果集非常小,可以立即返回。几个星期过去以后,数据开始多起来了,一个月之后,这个查询耗费的时间越来越长。
对于大部分应用程序来说,这很可能就是那个“错误的”查询:真的需要在查询中返回数据集中的大部分内容吗?大部分应用程序(尤其是拥有非常大的数据集的应用程序)都不需要。然而,也有一些合理的情况,可能需要得到大部分或者全部的数据:也许需要将这些数据导出到报表系统,或者是放在批量任务中。在这些情况下,应该尽可能快地返回数据集中的内容。
可以用{"$natural" : 1}强制数据库做全表扫描。6.1节会介绍$natural,它可以指定文档按照磁盘上的顺序排列。特别地,$natural可以强制MongoDB做全表扫描:
> db.entries.find({"created_at" : {"$lt" : hourAgo}}).hint({"$natural" : 1})
使用"$natural"排序有一个副作用:返回的结果是按照磁盘上的顺序排列的。对于一个活跃的集合来说,这是没有意义的:随着文档体积的增加或者缩小,文档会在磁盘上进行移动,新的文档会被写入到这些文档留下的空白位置。但是,对于只需要进行插入的工作来说,如果要得到最新的(或者最早的)文档,使用$natural就非常有用了。
上一篇文章:MongoDB指南---11、使用复合索引、$操作符如何使用索引、索引对象和数组、索引基数
下一篇文章:MongoDB指南---13、索引类型
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。